package com.slice.upload.strategy;

import com.slice.upload.autoconfigure.SliceUploadProperties;
import com.slice.upload.base.BaseStrategy;
import com.slice.upload.entity.FileUploadRespond;
import com.slice.upload.service.FileService;
import com.slice.upload.service.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.File;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.Map;

import static com.slice.upload.base.RedisKey.MD5;
import static com.slice.upload.base.RedisKey.UPLOAD;


/**
 * @ClassName BaseSliceUploadStrategy
 * @Description 分片上传策略基类
 * @Author frinder
 * @Date 2021/1/25 20:58
 * @Verion 1.0
 */
@Slf4j
public class BaseSliceUploadStrategy extends BaseStrategy<FileUploadPayload> {

    @Resource
    protected FileService fileService;

    @Resource
    protected RedisService redisService;

    @Resource
    protected SliceUploadProperties properties;

    protected int defaultChunkSize;

    @PostConstruct
    public void init() {
        this.defaultChunkSize = properties.getChunkSize() * 1024 * 1024;
    }

    /**
     * 设置处理结果
     *
     * @param payload
     */
    protected void setRespond(FileUploadPayload payload) throws Exception {
        // 此代码需要放在 try(){}catch(){} 外，需要等上面文件句柄关闭才能重新打开
        FileUploadRespond respond = upload(payload);
        payload.setRespond(respond);
    }

    /**
     * 创建临时文件
     *
     * @param payload
     * @return
     */
    protected File createTmpFile(FileUploadPayload payload) {
        // 获取上传文件全路径（包含文件名称）
        String filePath = fileService.getFilePath(payload);
        String tmpFileName = filePath + "_tmp";
        return new File(tmpFileName);
    }

    /**
     * 获取进度存储配置文件
     *
     * @param payload
     * @return
     */
    protected File createConfFile(FileUploadPayload payload) {
        String fileName = fileService.getFilePath(payload) + ".conf";
        File confFile = new File(fileName);
        return confFile;
    }

    /**
     * 分片上传文件
     *
     * @param payload
     * @return
     */
    public FileUploadRespond upload(FileUploadPayload payload) throws Exception {
        // 检查上传是否完成
        boolean isComplete = checkUploadIsComplete(payload);
        // 上传完成
        if (isComplete) {
            // 获取临时文件
            File tmpFile = createTmpFile(payload);
            // 重命名文件并存库
            FileUploadRespond respond = renameFile(tmpFile, payload.getFile().getOriginalFilename());
            if (respond.isUploadComplete()) {
                log.info("文件[{}]上传完成[{}]上传路径[{}]", payload.getFile().getOriginalFilename(),
                        respond.isUploadComplete(),
                        respond.getPath()
                );
                //TODO 保存文件信息到数据库
            }
            return respond;
        }
        // 上传未完成
        // 获取未完成部分并返回
        String md5 = fileService.getFileMD5(payload.getFile());
        Map<Integer, String> map = new HashMap(1) {{
            put(payload.getChunk(), md5);
        }};
        return FileUploadRespond.builder().items(map).build();
    }

    /**
     * 检查文件上传是否完成
     *
     * @param payload
     * @return
     */
    public boolean checkUploadIsComplete(FileUploadPayload payload) throws Exception {
        // 获取上传进度存储文件
        File confFile = createConfFile(payload);
        // 分段标记文件进度
        try (RandomAccessFile file = new RandomAccessFile(confFile, "rw")) {
            // 把该分段标记为 true 表示完成
            // 创建conf文件文件长度为总分片数，每上传一个分块即向conf文件中写入一个127，
            // 那么没上传的位置就是默认0，已上传的就是 Byte.MAX_VALUE 127
            file.setLength(payload.getChunks());
            file.seek(payload.getChunk());
            file.write(Byte.MAX_VALUE);
            log.info("文件[{}]分片总数[{}]分片[{}]上传完成", payload.getFile().getOriginalFilename(), payload.getChunks(), payload.getChunk());
        }
        // 完成标识
        byte isCompleted = Byte.MAX_VALUE;
        // 检查是否全部完成，如果数组里是否全部都是127(全部分片都成功上传)
        byte[] processItems = FileUtils.readFileToByteArray(confFile);
        for (int i = 0; i < processItems.length && isCompleted == Byte.MAX_VALUE; i++) {
            // 与运算，如果有部分没有完成则 isCompleted 不是 Byte.MAX_VALUE
            isCompleted = (byte) (isCompleted & processItems[i]);
            log.debug("分片[{}]是否完成[{}]", i, isCompleted);
        }
        // 最终为 127 时表示完成
        boolean result = isCompleted == Byte.MAX_VALUE;
        // 设置上传进度
        setUploadProgress(payload, confFile, result);
        return result;
    }

    /**
     * 把上传进度信息存进 redis
     *
     * @param payload
     * @param confFile
     * @param isCompleted
     * @return
     */
    private void setUploadProgress(FileUploadPayload payload, File confFile, boolean isCompleted) {
        if (isCompleted) {
            // 设置上传完成
            redisService.hset(UPLOAD.getVal(), payload.getMd5(), "true");
            // 删除进度配置文件路径
            redisService.del(MD5.getVal() + payload.getMd5());
            // 删除进度配置文件
            confFile.delete();
        } else {
            if (redisService.hHasKey(UPLOAD.getVal(), payload.getMd5())) {
                // 缓存数据已存在，则不处理
            } else {
                // 设置上传状态 未完成
                redisService.hset(UPLOAD.getVal(), payload.getMd5(), "false");
                // 设置配置文件路径
                redisService.set(MD5.getVal() + payload.getMd5(), confFile.toPath().toString());
            }
        }
    }

    /**
     * 文件重命名
     *
     * @param tmpFile  将要修改名字的文件
     * @param fileName 新的名字
     */
    private FileUploadRespond renameFile(File tmpFile, String fileName) {
        // 文件存在 & 类型为文件
        if (tmpFile.exists() && tmpFile.isFile()) {
            // 临时文件存放目录
            String parentPath = tmpFile.getParent();
            // 构建目标文件
            File destFile = new File(parentPath, fileName);
            // 修改文件名
            boolean uploadFlag = tmpFile.renameTo(destFile);
            log.info("更新临时文件结果[{}]临时文件[{}]重命名为[{}]", uploadFlag, tmpFile.toPath(), destFile.toPath());
            return FileUploadRespond.builder()
                    .uploadComplete(uploadFlag)
                    .mtime(System.currentTimeMillis() / 1000)
                    .path(destFile.toPath().toString())
                    .size(destFile.length())
                    .fileExt(FilenameUtils.getExtension(fileName))
                    .fileId(fileName)
                    .build();
        } else {
            log.warn("文件[{}]不存在，请检查", tmpFile.getName());
            return FileUploadRespond.builder().uploadComplete(false).build();
        }

    }

}
